﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using DTBuilderLib.VisualHierarchyClasses;
using System.Windows.Media.Imaging;
using System.Drawing;
using IndicoInterface.SimpleAgendaDataModel;
using Utilities;

namespace DTBuilderLib
{
    /// <summary>
    /// Helps with building the layout of the images and talks and sessions, etc.
    /// </summary>
    public class LayoutBuilder
    {
        /// <summary>
        /// What DPI are we going to allow here?
        /// </summary>
        public int DPI { get; set; }

        /// <summary>
        /// How big, in inches, between the slides in the x direction?
        /// </summary>
        public float XBoarderSize { get; set; }

        /// <summary>
        /// How big, in inches, between the slides in the y direction?
        /// </summary>
        public float YBoarderSize { get; set; }

        /// <summary>
        /// What color should be square that does the meeting background be?
        /// </summary>
        public Color MeetingBackgroundColor { get; set; }

        /// <summary>
        /// Get/Set the color of the session background.
        /// </summary>
        public Color SessionBackgroundColor { get; set; }

        /// <summary>
        /// Get/Set the color of the talk background
        /// </summary>
        public Color TalkBackgroundColor { get; set; }

        /// <summary>
        /// Get/Set the number of slides per row. This isn't exact,
        /// it will be varried by +- 20% in order to make a better layout.
        /// </summary>
        public int DefaultSlidesPerRow { get; set; }

        /// <summary>
        /// Get/Set the path where we can put the background images we are going to be
        /// creating to make this layout.
        /// </summary>
        public string ImageCreationPath { get; set; }

        /// <summary>
        /// How many rows should we set per talk chunk max - this is to avoid
        /// a memory problem later down the track. We chunk up the things we
        /// display this way.
        /// </summary>
        public int MaxRowsPerTalkChunk { get; set; }

        /// <summary>
        /// Setup defaults
        /// </summary>
        public LayoutBuilder()
        {
            DPI = 600;
            XBoarderSize = 0.2F;
            YBoarderSize = 0.2F;
            DefaultSlidesPerRow = 10;
            MaxRowsPerTalkChunk = 3;

            MeetingBackgroundColor = Color.FromArgb(255, 200, 200, 200); // Noth quite light gray...
            SessionBackgroundColor = Color.DarkGray;
            TalkBackgroundColor = Color.Gray;

            ImageCreationPath = Path.GetTempPath();
        }

        /// <summary>
        /// Returns a visual hierarchy for the trees. This really just converts and lays
        /// out the slides.
        /// </summary>
        /// <param name="slideList"></param>
        /// <returns></returns>
        Visual BuildVisualTreeForTalkSlides(List<FileInfo> slideList)
        {
            ///
            /// Convert them to the VisualImages. Protect against
            /// a malformed jpeg.
            /// 

            var visualImages = from s in slideList
                               let img = ProtectedVSICreator(s)
                               where img != null
                               select img;

            ///
            /// Next, lay them out. Organizing this is a bit tricky. We need to figure out
            /// how many columns we need. Once we have that we know how many rows we need. We
            /// can then figure out if we need to split the talk up into multiple visual guys
            /// 

            int ncols = CalcColumnsCloseToTarget(slideList.Count, DefaultSlidesPerRow);

            var slideChunks = from vgroup in visualImages.ToList().GetListAsChunks(ncols*MaxRowsPerTalkChunk)
                           select vgroup;

            var visualChunks = from s in slideChunks
                               select LayoutByColumns(ncols, s.ToArray());

            ///
            /// What we do next depends on how many of these chunks we have run into. If just one
            /// then things are pretty easy.
            /// 

            var visualChunksAsArray = visualChunks.ToArray();
            if (visualChunksAsArray.Length == 1)
            {
                return visualChunksAsArray[0];
            }

            ///
            /// Mark them all as a sub-composition, and lay them out in a holder, and
            /// return that!
            /// 

            VisualHolder allChunks = new VisualHolder();
            double offset = 0.0;
            foreach (var v in visualChunksAsArray)
            {
                VisualBitmapImage bm = new VisualBitmapImage(v.Width, v.Height,
                    100, TalkBackgroundColor);

                VisualHolder subImages = new VisualHolder();
                v.IncZIndex();
                subImages.AddImage(v);
                subImages.AddImage(bm);

                subImages.IsCompositionUnit = true;
                subImages.YPosition = offset;
                allChunks.AddImage(subImages);
                offset = allChunks.Height + YBoarderSize;
            }

            return allChunks;
        }

        /// <summary>
        /// Create a new visual single image. If we fail, return null.
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private Visual ProtectedVSICreator(FileInfo s)
        {
            try
            {
                return new VisualSingleImage(s, DPI);
            }
            catch (Exception e)
            {
                Logger.LogErrorMessage("BADJPEG", "Bad jpeg in file; was not able to open and read it: " + s.FullName + " -- " + e.Message);
                return null;
            }
        }

        /// <summary>
        /// Do the actual layout work when we are doing things by columns, cross first, then down.
        /// </summary>
        /// <param name="visualImages">A list of the images to layout</param>
        /// <param name="ncols">Number of columns to use in the layout</param>
        /// <returns></returns>
        private Visual LayoutByColumns(int ncols, Visual[] visualImages)
        {
            double current_x_pos = 0.0F;
            double current_y_pos = 0.0F;
            double next_y_pos = 0.0F;
            int current_column = 0;
            int picture_index = 0;
            Func<Visual, Visual> placeit = s =>
            {
                s.XPosition = current_x_pos;
                s.YPosition = current_y_pos;
                s.IncZIndex(picture_index);

                picture_index += 1;

                double ynext = current_y_pos + s.Height;
                if (ynext > next_y_pos)
                {
                    next_y_pos = ynext;
                }
                current_x_pos += s.Width + XBoarderSize;
                current_column += 1;
                if (current_column >= ncols)
                {
                    current_column = 0;
                    current_x_pos = 0.0F;
                    current_y_pos = next_y_pos + YBoarderSize;
                }
                return s;
            };

            var placedSlides = from s in visualImages
                               select placeit(s);

            VisualHolder result = new VisualHolder();
            foreach (var v in placedSlides)
            {
                result.AddImage(v);
            }
            return result;
        }

        /// <summary>
        /// We calculate the number of columns to put these guys in. The idea is we have a list of
        /// same-size slides, and we want to arrange them in long vertical columns so that they fit
        /// inside the height of one slide and are about compressedToHeight wide.
        /// </summary>
        /// <param name="height">Height of all slides, includeing the title slide</param>
        /// <param name="width">How wide all the slides are</param>
        /// <param name="nslides">Number of slides we are dealing with</param>
        /// <param name="compressedToWidth">Width to compress each column to</param>
        /// <returns></returns>
        private int CalcColumnsHeightWhenShrunk(float height, float width, int nslides, float compressedToHeight)
        {
            ///
            /// First, figure out the shrink factor. This is what we will have to do in order to get the slides to the
            /// correct width.
            /// 

            double shrinkFactor = compressedToHeight / width;

            ///
            /// The resulting height of the slides after they are shrunk.
            /// 

            double shrunkHeight = height * shrinkFactor;

            ///
            /// And the number of slides per column, and then the number of columns...
            /// We are rounding down on purpose...
            /// 

            double slidesPerCol = height / shrunkHeight;
            double ncols = ((double)nslides) / slidesPerCol;
            return (int)(ncols + 1.0F);
        }

        /// <summary>
        /// We calculate the number of columns to put these guys in. The idea is we have a list of
        /// same-size slides, and we want to arrange them in long horizontal rows so that they fit
        /// inside the width of one slide and are about compressedToHeight high.
        /// </summary>
        /// <param name="height">Height of all slides, includeing the title slide</param>
        /// <param name="width">How wide all the slides are</param>
        /// <param name="nslides">Number of slides we are dealing with</param>
        /// <param name="compressedToWidth">Width to compress each column to</param>
        /// <returns></returns>
        private int CalcColumnsWidthWhenShrunk(float height, float width, int nslides, float compressedToHeight)
        {
            ///
            /// First, figure out the shrink factor. This is what we will have to do in order to get the slides to the
            /// correct height.
            /// 

            float shrinkFactor = compressedToHeight / height;

            ///
            /// The resulting width of the slides after they are shrunk.
            /// 

            float shrunkWidth = width * shrinkFactor;

            ///
            /// And the number of slides per column, and then the number of columns...
            /// We are rounding down on purpose...
            /// 

            float slidesPerRow = width / shrunkWidth;
            return (int)(slidesPerRow);
        }

        /// <summary>
        /// How many colunms should we display? In the ideal world we would have three rows...
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private int CalculateNumberOfColunms2x3(int nslides)
        {
            /// Short talk gets special treatment
            if (nslides <= 2.0)
                return nslides;

            /// Try to make it 3:2, 3 is wide...

            float xcol = (float)Math.Sqrt(3.0F / 2.0F * nslides);
            return (int)(xcol + 0.5F);
        }

        /// <summary>
        /// Calculate how many columns we should render this talk in given the number of slides, along with
        /// a target number we'd like to aim for.
        /// </summary>
        /// <param name="nslides"></param>
        /// <param name="targetColumns"></param>
        /// <returns></returns>
        private int CalcColumnsCloseToTarget(int nslides, int targetColumns)
        {
            /// Should it all be on one line? Yes, if it is smaller than the target!

            if (nslides <= targetColumns)
            {
                return nslides;
            }

            /// Now, we move +- 20% of the given target and see what fits the
            /// best with what we are given.

            int delta = (int)((float)targetColumns * 0.2F);

            var allofthem = from ncol in CalcDeltaRange(targetColumns, delta)
                            orderby ncol - SlidesInLastRow(nslides, ncol)
                            select ncol;

            return allofthem.First();
        }

        /// <summary>
        /// Calc the number of slides in the last row given # of slides and # of columns. Helps
        /// calc the optimal layout.
        /// </summary>
        /// <param name="nslides"></param>
        /// <param name="ncol"></param>
        /// <returns></returns>
        private int SlidesInLastRow(int nslides, int ncol)
        {
            int remainder = nslides % ncol;
            if (remainder == 0)
            {
                return ncol;
            }
            return remainder;
        }

        /// <summary>
        /// Calc a delta array we can iterate over.
        /// </summary>
        /// <param name="targetColumns"></param>
        /// <param name="delta"></param>
        /// <returns></returns>
        private int[] CalcDeltaRange(int targetColumns, int delta)
        {
            int[] result = new int[delta * 2 + 1];

            int index = 0;
            for (int i = -delta; i <= delta; i++)
            {
                result[index] = targetColumns + i;
                index = index + 1;
            }

            return result;
        }

        /// <summary>
        /// Setup a talk with title and author, etc., that shows everything.
        /// </summary>
        /// <param name="talktitle">Title to be put at top of talk</param>
        /// <param name="talkInfoString">Info (like speaker and also time, etc., put under the title)</param>
        /// <param name="talkSlides"></param>
        /// <returns></returns>
        private Visual BuildVisualTreeForATalk(string talktitle, string shortname, string talkInfoString, List<FileInfo> talkSlides)
        {
            ///
            /// Convert the slides themselves. If there are sub-images then we
            /// have to be careful not to mark the upper guys as such...
            /// 

            Visual allSlides = BuildVisualTreeForTalkSlides(talkSlides);

            int nComposable = 0;
            foreach (var v in allSlides.GetHierarchicalEnumerator(mv => mv.IsCompositionUnit))
            {
                if (v.IsCompositionUnit)
                {
                    nComposable = nComposable + 1;
                    v.ShortName = shortname + "_chunk_" + nComposable.ToString("00");
                }
            }

            ///
            /// Create the title slide -- this will sidle-up next to the
            /// real slides. Make sure the title slide is not "too small".
            ///

            Visual titleSlide = new VisualSingleImage(talkSlides[0].CopyToTempFile(), DPI);
            titleSlide.ShortName = shortname + "_titleslide";
            double rawSlideWidth = titleSlide.Width;
            if (titleSlide.Width < 10.0)
            {
                titleSlide.Scale(10.0F / titleSlide.Width);
            }

            ///
            /// Make sure a single row expands to fit the title slide width.
            /// 

            double rowWidth = (rawSlideWidth * (DefaultSlidesPerRow + 0.2F)) - 0.2F;
            double factor = titleSlide.Width / rowWidth;
            allSlides.Scale(factor);
#if false
            if (allSlides.Height > titleSlide.Height)
            {
                factor = titleSlide.Height / allSlides.Height;
                allSlides.Scale(factor);
            }
#endif

            ///
            /// If they are still too wide -- which can happen if the +- 20% fit rule makes
            /// them more than DefaultSlidesPerRow, shrink appropriately.
            /// 

            if (allSlides.Width > titleSlide.Width)
            {
                factor = titleSlide.Width / allSlides.Width;
                allSlides.Scale(factor);
            }

            ///
            /// Next, build the background and the title slide.
            /// 

            VisualHolder slideContent = new VisualHolder();
            MakeSlideSizeReasonable(titleSlide);
            slideContent.AddImage(titleSlide);

            ///
            /// No need to add the rest fo the slides if there is only the title slide!
            /// 

            if (talkSlides.Count > 1)
            {
                allSlides.YPosition = titleSlide.Height + YBoarderSize;
                double delta = (titleSlide.Width - allSlides.Width) / 2.0F;
                allSlides.XPosition = delta;
                slideContent.AddImage(allSlides);
            }

            ///
            /// Ok -- now we do the background. This has to include the title, and should be
            /// bigger than the whole big slide. :-)
            /// 

            slideContent.IncZIndex();
            VisualBitmapImage bm = new VisualBitmapImage(slideContent.Width + 2.0F * XBoarderSize,
                slideContent.Height + 1.0F + 2.0F * YBoarderSize,
                300, TalkBackgroundColor);
            bm.AddText(0.0F, 0.0F, 0.5F, talktitle);
            bm.AddText(0.0F, 0.6F, 0.3F, talkInfoString);
            bm.ShortName = shortname + "_talk_background";

            VisualHolder wholeTalk = new VisualHolder();
            wholeTalk.AddImage(bm);
            slideContent.XPosition = XBoarderSize;
            slideContent.YPosition = 1.0F + YBoarderSize;
            wholeTalk.AddImage(slideContent);

            if (nComposable == 0)
            {
                wholeTalk.IsCompositionUnit = true;
            }

            ///
            /// Set the meta information
            /// 

            wholeTalk.Title = talktitle;
            wholeTalk.ShortName = shortname;

            return wholeTalk;
        }

        /// <summary>
        /// Make sure the slide isn't "too" big or something weird like that.
        /// </summary>
        /// <param name="slide"></param>
        private static void MakeSlideSizeReasonable(Visual slide)
        {
            if (slide.Width > 11.0F)
            {
                slide.Scale(11.0F / slide.Width);
            }
            if (slide.Height > 11.0F)
            {
                slide.Scale(11.0F / slide.Height);
            }
        }

        /// <summary>
        /// Given a list of talks, render the visuals for them.
        /// </summary>
        /// <param name="talks"></param>
        /// <param name="slideLookup"></param>
        /// <returns></returns>
        public Visual BuildVisualTreeForASession(string sessionName, string sessionShortName, DateTime sessionStart, DateTime sessionEnd, Talk[] talks, Dictionary<Talk, List<FileInfo>> slideLookup)
        {
            ///
            /// A no-talk session gets no love from us.
            /// 

            if (talks == null || talks.Length == 0)
            {
                return null;
            }

            ///
            /// Turn the talks into rendered visuals
            /// 

            var renderedTalks = from t in talks
                                where slideLookup[t].Count != 0
                                select BuildVisualTreeForATalk(t.Title, t.ID, GenerateTalkInfoString(t), slideLookup[t]);

            ///
            /// Make sure they are the "right" size - that things don't get too big!
            /// 

            var renderedTalksRescaled = from t in renderedTalks
                                        select SceneHelpers.getSizeRight(t);

            ///
            /// If there is nothing there, then bail out because we have nothing to render!
            /// 

            var renderedTalksRescaledAsArray = renderedTalksRescaled.ToArray();
            if (renderedTalksRescaledAsArray.Length == 0)
            {
                return null;
            }

            ///
            /// Next, do the layout.
            /// 

            Visual allTalks = LayoutVisuals23(renderedTalksRescaledAsArray);
            MakeSlideSizeReasonable(allTalks);

            ///
            /// Now, the session title and background.
            /// 

            allTalks.IncZIndex();
            VisualBitmapImage bm = new VisualBitmapImage(allTalks.Width + XBoarderSize * 2.0F, allTalks.Height + YBoarderSize * 2.0F + 1.0F,
                300, SessionBackgroundColor);
            bm.AddText(0F, 0F, 0.5F, sessionName);
            bm.AddText(0F, 0.6F, 0.3F, GenerateDeltaTimeStringWithDate(sessionStart, sessionEnd));
            bm.ShortName = sessionShortName + "_session_background_" + sessionStart.ToString("yyyy'-'MM'-'dd HH'-'mm'-'ss");

            VisualHolder session = new VisualHolder();
            session.AddImage(bm);
            allTalks.XPosition = XBoarderSize;
            allTalks.YPosition = 1.0F + YBoarderSize;
            session.AddImage(allTalks);

            session.Title = sessionName;
            session.ShortName = sessionShortName;

            return session;
        }

        /// <summary>
        /// Generates the "info-string" for a talk -- this is the speaker, start time, etc.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private string GenerateTalkInfoString(Talk t)
        {
            StringBuilder bld = new StringBuilder();
            bool addedSpeaker = false;
            if (t.Speakers != null)
            {
                foreach (var speaker in t.Speakers)
                {
                    bld.AppendFormat("{0} ", speaker);
                    addedSpeaker = true;
                }
            }

            if (addedSpeaker)
            {
                bld.Append(" - ");
            }
            bld.Append(GenerateDeltaTimeString(t.StartDate, t.EndDate));

            return bld.ToString();
        }

        /// <summary>
        /// Given a start and end time, generate a nice delta string.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private static string GenerateDeltaTimeString(DateTime startDate, DateTime endDate)
        {
            StringBuilder bld = new StringBuilder();
            bld.AppendFormat("{0} ", startDate.ToShortTimeString());

            TimeSpan sp = endDate - startDate;
            int minutes = ((sp.Days*24) + sp.Hours)*60 + sp.Minutes;
            if (sp.Days > 0)
            {
                decimal days = minutes / ((decimal)(60 * 24));
                bld.AppendFormat("({0:F1} days)", days);
            }
            else if (minutes > 60)
            {
                decimal hours = minutes / ((decimal)60);
                bld.AppendFormat("({0:F1} hours)", hours);
            }
            else
            {
                bld.AppendFormat("({0:F0} minutes)", minutes);
            }
            return bld.ToString();
        }

        /// <summary>
        /// Generate a date time string, but include the start date.
        /// </summary>
        /// <param name="dateTime"></param>
        /// <param name="dateTime_2"></param>
        /// <returns></returns>
        private string GenerateDeltaTimeStringWithDate(DateTime startTime, DateTime finishTime)
        {
            return startTime.ToLongDateString() + " - " + GenerateDeltaTimeString(startTime, finishTime);
        }
        /// <summary>
        /// Given a collections of visuals, lay them out in a 2:3 (approx) array.
        /// </summary>
        /// <param name="visual"></param>
        /// <returns></returns>
        private Visual LayoutVisuals23(Visual[] visual)
        {
            return LayoutByColumns(CalculateNumberOfColunms2x3(visual.Length), visual);
        }

        /// <summary>
        /// Layout a meeting.
        /// </summary>
        /// <param name="meeting"></param>
        /// <param name="slides"></param>
        /// <returns></returns>
        public Visual BuildVisualTreeForAMeeting (Meeting meeting, Dictionary<Talk, List<FileInfo>> slides)
        {
            ///
            /// Move the images we are going ot generate off into one place (nice and quiet...).
            /// 

            DirectoryInfo picDir = new DirectoryInfo(ImageCreationPath + "\\" + Path.GetRandomFileName());
            picDir.Create();
            VisualBitmapImage.GlobalImageWritingDirectory = picDir;

            ///
            /// Special case a single session -- becase we have to do something
            /// weird with the "session" title.
            /// 

            if (meeting.Sessions.Length == 1)
            {
                if (meeting.Sessions[0].Talks == null)
                {
                    return null;
                }
                var talks = from s in meeting.Sessions
                            from t in s.Talks
                            where slides.ContainsKey(t)
                            where t.SlideURL != null
                            where t.SlideURL.Length != 0
                            select t;
                return BuildVisualTreeForASession(meeting.Title, meeting.ID, meeting.StartDate, meeting.EndDate, talks.ToArray(), slides);
            }

            ///
            /// Visuals for each session. Null sessions are discarded.
            /// 

            var sessionVisualsAll = from t in meeting.Sessions
                                 select BuildVisualTreeForASession(t.Title, t.ID, t.StartDate, t.EndDate, t.Talks, slides);
            var sessionVisuals = from t in sessionVisualsAll
                                 where t != null
                                 select t;

            ///
            /// If there is nothig there, then we are outta here!
            /// 

            var sessionVisualsAsArray = sessionVisuals.ToArray();
            if (sessionVisualsAsArray.Length == 0)
            {
                return null;
            }

            ///
            /// Put them on a the usual 2:3 board
            /// 

            var meetingVisuals = SceneHelpers.getSizeRight(LayoutVisuals23(sessionVisualsAsArray));

            ///
            /// And stuff those into something a bit bigger...
            /// 

            VisualBitmapImage back = new VisualBitmapImage(meetingVisuals.Width + XBoarderSize * 2.0F,
                meetingVisuals.Height + YBoarderSize * 2.0F + 1.0F,
                300, MeetingBackgroundColor);
            back.AddText(0, 0, 0.5F, meeting.Title);
            back.AddText(0, 0.5F, 0.3F, GenerateDeltaTimeStringWithDate(meeting.StartDate, meeting.EndDate));
            back.ShortName = meeting.ID + "_meeting_background";

            VisualHolder vMeeting = new VisualHolder();
            vMeeting.AddImage(back);
            meetingVisuals.XPosition = XBoarderSize;
            meetingVisuals.YPosition = YBoarderSize + 1.0F;
            vMeeting.AddImage(meetingVisuals);

            vMeeting.ShortName = meeting.ID;
            vMeeting.Title = meeting.Title;

            return vMeeting;
        }
    }
}
